Skip to content

feat: add @metamask/profile-controller package - #10558

Merged
hmalik88 merged 79 commits into
mainfrom
hm/add-profile-controller
Sep 29, 2026
Merged

hmalik88 merged 79 commits into
mainfrom
hm/add-profile-controller

Conversation

@hmalik88

@hmalik88 hmalik88 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Explanation

  • Adds @metamask/profile-controller, a new package for managing MetaMask user profile state
  • ProfileService communicates with the MetaMask Profile API (profile.api.cx.metamask.io), exposing getProfile, createProfile, replaceProfile, updateProfile, deleteProfile, checkUsernameAvailability, getXAuthUrl, connectX, and getXAccount via the messenger, with superstruct validation on all inputs and responses
  • ProfileController manages profile state derived from the API and exposes all operations via the messenger; also exports useGetProfile and useCheckUsernameAvailability React hooks for UI components via @metamask/react-data-query

References

N/A

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Adds authenticated profile CRUD, persisted user state, and X OAuth flows; mistakes could affect account identity data, though inputs/outputs are struct-validated and heavily tested.

Overview
Introduces @metamask/profile-controller, a new monorepo package for MetaMask user profiles (username, bio, avatar, linked addresses, trading privacy) and optional X (Twitter) linking.

ProfileService (extends BaseDataService) calls the MetaMask Profile API with bearer auth from AuthenticationController:getBearerToken, validates requests/responses with superstruct, and exposes CRUD, username availability, and X OAuth (getXAuthUrl, connectX, getXAccount) via messenger actions.

ProfileController keeps persisted profile / optional xProfile state, maps API shapes to UI-friendly types, and delegates mutations to ProfileService (including guards when no profile exists and clearing state on delete). Broad Jest coverage is included for both layers.

Repo wiring adds CODEOWNERS (@MetaMask/accounts-engineers), README/package graph entries, root/tsconfig references, teams.json, and oxlint suppression for the new jest config.

Reviewed by Cursor Bugbot for commit dc46832. Bugbot is set up for automated code reviews on this repo. Configure here.

@hmalik88
hmalik88 marked this pull request as ready for review September 29, 2026 01:04
@hmalik88
hmalik88 deployed to default-branch September 29, 2026 01:05 — with GitHub Actions Active

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/profile-controller/src/ProfileController.ts Outdated
Comment thread packages/profile-controller/src/ProfileService.ts

@mcmire mcmire left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Most comments are minor and can be deferred to another PR. Only big question is around whether or not it makes sense to include React hooks in the same package.

Comment thread packages/profile-controller/src/ProfileController.ts Outdated
Comment thread packages/profile-controller/src/ProfileService.ts Outdated
Comment thread packages/profile-controller/src/ProfileService.ts
Comment thread packages/profile-controller/src/ProfileService.ts Outdated
* @param identifier - The profile identifier to fetch.
* @returns A TanStack Query result containing the profile data.
*/
export function useGetProfile(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... we have tried to keep APIs that can be used within the background/"engine" of the client and APIs that can be used on the UI side in separate packages. This way if you want to use the controller/service in a project that doesn't use React (e.g. the agentic wallet) you can do so. That's why @metamask/react-data-query is a separate package from @metamask/base-data-service, for instance.

What are your thoughts on creating two packages? Or maybe we can just put these hooks in the client for now and extract them to a new package later?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dc46832 That makes sense to split into a diff package. I can do a follow up PR later with the separate package. Looping in @joaosantos15 since this was a request from their team.

Comment thread packages/profile-controller/LICENSE.APACHE2 Outdated
Mrtenz
Mrtenz previously requested changes Sep 29, 2026

@Mrtenz Mrtenz left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just merged #10564. Can you apply the same change to the package.json here?

Comment thread packages/profile-controller/package.json Outdated
Comment thread packages/profile-controller/package.json Outdated
@hmalik88
hmalik88 dismissed stale reviews from ccharly and gantunesr via 5307c10 September 29, 2026 19:51
@hmalik88
hmalik88 dismissed Mrtenz’s stale review September 29, 2026 21:39

addressed comments

Comment thread packages/profile-controller/CHANGELOG.md Outdated

@mcmire mcmire left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@hmalik88
hmalik88 added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit b0d38fc Sep 29, 2026
344 checks passed
@hmalik88
hmalik88 deleted the hm/add-profile-controller branch September 29, 2026 22:11
@joaosantos15

Copy link
Copy Markdown
Contributor

@metamaskbot publish-preview

Naz-Ovh pushed a commit to 0x-fork/metamask-core that referenced this pull request Oct 12, 2026
MetaMask#10668)

## Explanation

`@metamask/profile-controller` could start an X (Twitter) link, but the
client had to supply the profile ID and hold the OAuth details. The
Profile API now owns the whole X OAuth flow: it keeps the client secret,
the PKCE verifier and the X tokens, and it creates the MetaMask profile
during the connect if one doesn't exist yet (using the X handle as the
username). The client only opens the authorize URL and relays the
returned `code` and `state`.

This PR updates the controller and service to match:

- **`ProfileController.startXConnect({ linkedAddress? })`** (new)
returns `{ authorizationUrl, state }`. Nothing is stored in controller
state. The profile comes from the verified bearer token on the backend.
`linkedAddress` (CAIP-10) is forwarded as `linked_address` and is
required when the profile doesn't exist yet.
- **`ProfileController.connectX({ code, state, profileId })`**
(**BREAKING**) now returns `{ profile, xProfile, profileCreated }`
instead of just the X profile.
- It saves `xProfile` to state as soon as the connect succeeds, then
fetches the profile, because the backend may have just created it.
- If that fetch fails, the X link stays saved and a clear error is
thrown.
- **`ProfileController.disconnectX(profileId)`** (new) clears `xProfile`
and sets `connectedToX` to `false`.
- **`ProfileService.disconnectX(profileId)`** (new) sends `DELETE
profiles/{profileId}/x`. A `404` is treated as success, meaning the
account is already disconnected; otherwise a client whose saved state
has drifted from the backend could never disconnect.
- **`ProfileService.getXAuthUrl(linkedAddress?)`** sends the optional
`linked_address` query parameter.
- **`ProfileService.connectX`** accepts the optional `profile_created`
boolean in the response. It's optional so older backends still validate.

`startXConnect` and `disconnectX` are exposed on the messenger, and the
method action types are regenerated.

## References

- Builds on MetaMask#10558 (`@metamask/profile-controller`).
- Mobile consumer PR: MetaMask/metamask-mobile#37160
- Related to
https://consensyssoftware.atlassian.net/browse/MUL-2317?atlOrigin=eyJpIjoiYzRmMzRiZjY5MDY2NGY1NjljNjc3OGNkYjBkMmE1MGYiLCJwIjoiaiJ9

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [x] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Breaking changes to X connect/createProfile state handling and OAuth
entry requirements will affect mobile and other consumers; OAuth unlink
uses authenticated DELETE with intentional 404-as-success semantics.
> 
> **Overview**
> **Adds X OAuth start and unlink APIs** on
`ProfileController`/`ProfileService`, and tightens when X link state is
written to controller state.
> 
> `getXAuthorizationUrl` starts the flow: it requires a profile already
in state, calls the backend for the authorize URL and `state`, and
returns ephemeral `ConnectXSession` data without persisting OAuth
details in the controller. `disconnectX(profileId)` calls `DELETE
profiles/{profileId}/x` (404 treated as success) and clears `xProfile` /
`connectedToX` when the ID matches state.
> 
> **Breaking behavior changes:** `connectX` now requires a profile in
state and, on success, immediately sets `xProfile` and `connectedToX`
(it no longer relies on `createProfile` to carry over an `x_profile`
from the create response). `getXAuthUrl` is bearer-token–scoped on the
server with no `linkedAddress` parameter.
> 
> Also fixes the messenger event typing to
`ProfileController:stateChanged`, renames `XConnectResponse` →
`ConnectXResponse`, and invalidates cached `getProfile` after
connect/disconnect mutations in `ProfileService`.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
7ec47a5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants